package ija.xls.xml.game;

import ija.model.basis.Figure;
import ija.model.figures.Piece;
import ija.model.figures.Queen;
import ija.model.history.HistoryMove;
import ija.model.players.ComputerPlayer;
import ija.model.players.LocalPlayer;
import ija.model.players.Player;
import ija.model.players.RemotePlayer;
import ija.model.types.FigureColor;
import ija.model.types.FigureDirection;
import ija.xls.notation.DeskCoordinate;
import ija.xls.notation.NotationReader;
import ija.xls.notation.NotationReaderFactory;
import ija.xls.xml.game.FigureDescription.FigureType;
import java.io.*;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.dom4j.*;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;

/**
 * Trida definuje objekt ktery bude slouzit pro nacitani/ukladani partie z/do
 * xml souboru.
 * 
 * @author Radek Necas, xnecas18
 */
public class XmlGameReaderWriter {

    protected Document docOut;
    protected Document docIn;

    protected NotationReaderFactory.NotationType notationType;
    protected NotationReader notation;

    /**
    * Vytvori prazdny reader-writer
    */
    public XmlGameReaderWriter() {
        this.docOut = null;
        this.docIn = null;
        this.notationType = null;
    }

    /**
    * Inicializuje reader - cast co bude nacitat z xml.
    * 
    * @param filename
    *            nazev souboru, ze ktereho se bude nacitat
    * @throws DocumentException
    *             chyba pri nacitani souboru
    */
    public void initInput(String filename) throws DocumentException {
        SAXReader reader = new SAXReader();
        this.docIn = reader.read(filename);
        this.setNotationTypeFromFile();
    }

    /**
    * Inicializuje reader - cast co nacita xml.
    * 
    * @param xml
    *            string s xml obsahem
    * @throws DocumentException
    *             chyba pri praci se streamy
    */
    public void initInputFromString(String xml) throws DocumentException {
        SAXReader reader = new SAXReader();
        InputStream in = new ByteArrayInputStream(xml.getBytes());
        this.docIn = reader.read(in);
    }

    /**
    * Inicializuje writer - cast co bude zapisovat do xml.
    * 
    * @param notationType
    *            typ notace, ktera bude vyuzita. Touto notaci bude tah zapsan i
    *            cten z objektu.
    */
    public void initOutput(NotationReaderFactory.NotationType notationType) {
        this.notationType = notationType;
        this.docOut = DocumentHelper.createDocument();
        Element root = this.docOut.addElement("IjaGame");
        root.addElement("date");
        root.addElement("type").addText("Checkers");
        root.addElement("state");
        root.addElement("winner");
        root.addElement("starter");
        root.addElement("activePlayer");
        root.addElement("notation").addText(notationType.toString());

        root.addElement("players");
        root.addElement("history");
        root.addElement("desk");
    }

    /**
    * Metoda nacita z xml textovou hodnotu elementu daneho xPath.
    * 
    * @param xPath
    *            cesta k elementu
    * @return textovy obsah elementu nebo null, pokud takovy element nebyl
    *         nalezen
    */
    private String getStringItem(String xPath) {
        if (this.docIn != null) {
                Node node = this.docIn.selectSingleNode(xPath);
                if (node != null) {
                        return node.getText();
                }
        }

        return null;
    }

    /**
    * Metoda ziska datum z xml.
    * 
    * @return datum xml dokumentu
    */
    public String getGameDate() {
        return this.getStringItem("/IjaGame/date");
    }

    /**
    * @return typ hry ulozene v xml.
    */
    public String getGameType() {
        return this.getStringItem("/IjaGame/type");
    }

    /**
    * @return stav hry ulozene v xml.
    */
    public String getGameState() {
        return this.getStringItem("/IjaGame/state");
    }

    /**
    * @return id vitezneho hrace.
    */
    public int getGameWinnerId() {
        String id = this.getStringItem("IjaGame/winner");
        if (id != null) {
                return Integer.parseInt(id);
        }

        return -1;
    }

    /**
    * @return id hrace co zacinal hru.
    */
    public int getStartPlayerId() {
        String id = this.getStringItem("IjaGame/starter");
        if (id != null) {
                return Integer.parseInt(id);
        }

        return -1;
    }

    /**
    * @return id aktivniho hrace. Tento hrac by mel byt aktivni po obnoveni
    *         hry.
    */
    public int getActivePlayerId() {
        String id = this.getStringItem("IjaGame/activePlayer");
        if (id != null) {
                return Integer.parseInt(id);
        }

        return -1;
    }

    /**
    * @return typ notace kterou jsou ulozeny tahy v xml a ktera by se mela
    *         pouzivat pri obnoveni hry.
    */
    public NotationReaderFactory.NotationType getNotationType() {
        String not = this.getStringItem("/IjaGame/notation");
        return NotationReaderFactory.NotationType.getFromString(not);
    }

    /**
    * @return barvu vrchniho hrace
    */
    public FigureColor getTopPlayerColor() {

        Node deskNode = this.docIn.selectSingleNode("/IjaGame/desk[@topPlayerColor]");
        if (deskNode == null) {
                return null;
        }
        Element e = (Element) deskNode;
        String c = e.attributeValue("topPlayerColor");
        return FigureColor.getFromString(c);
    }

    /**
    * @return xml vstup jako string
    */
    public String inputAsXml() {
        return this.docIn.asXML();
    }

    /**
    * @return xml vystup jako string
    */
    public String outputAsXml() {
        return this.docOut.asXML();
    }

    /**
    * Metoda nastavi typ notace z xml souboru.
    */
    public void setNotationTypeFromFile() {
        this.notationType = this.getNotationType();
    }

    /**
    * Ziska iterator pro pruchod vsemi hracu v xml.
    * 
    * @return iterator prochazejici vsechny ulozene hrace v xml
    */
    public Iterator<Player> playersIterator() {
        return new PlayersIterator();
    }

    /**
    * Ziska iterator pro pruchod vsemi tahy v historii tahu.
    * 
    * @return iterator prochazejici historii tahu.
    */
    public Iterator<HistoryMove> historyMovesIterator() {
        return new HistoryIterator();
    }

    /**
    * Ziska iterator pro pruchod vsemi figurkami v xml. Tento typ iteratoru se
    * vyuziva, pokud je v dokumentu ulozena nedohrana hra a je treba nacist
    * posledni stav hry. Pote timto iteratorem ziskame vsechny figurky dane
    * barvy.
    * 
    * @param c
    *            barva figurek, ktere chceme ziskat
    * @return
    */
    public Iterator<FigureDescription> figuresIterator(FigureColor c) {
        return new FigureIterator(c);
    }

    /**
    * Metoda nastavi prazdny podelement korene daneho jmeno.
    * 
    * @param elemName
    *            jmeno pridavaneho elementu.
    * @return odkaz na novy element nebo null pri chybe
    */
    private Element addSubRootEmptyElement(String elemName) {
        if (elemName == null) {
                return null;
        }
        Element root = this.docOut.getRootElement();
        return root.addElement(elemName);
    }

    /**
    * Prida datum do xml
    * 
    * @param date
    *            textova reprezentace datumu
    */
    public void addDateElement(String date) {
        if (date == null) {
                return;
        }

        Element dateEl = (Element) this.docOut.selectSingleNode("/IjaGame/date");
        if(dateEl == null) {
            Element root = this.docOut.getRootElement();
            dateEl = root.addElement("date");
        }

        dateEl.addText(date);
    }

    /**
    * Prida typ hry do xml
    * 
    * @param type
    *            typ hry
    */
    public void addTypeElement(String type) {
        if (type == null) {
                return;
        }

        Element typeEl = (Element) this.docOut.selectSingleNode("/IjaGame/type");
        if(typeEl == null) {
            Element root = this.docOut.getRootElement();
            typeEl = root.addElement("type");
        }

        typeEl.addText(type);
    }

    /**
    * Prida stav hry do xml
    * 
    * @param state
    *            stav hry
    */
    public void addStateElement(String state) {
        if (state == null) {
                return;
        }

        Element stateEl = (Element) this.docOut.selectSingleNode("/IjaGame/state");
        if(state == null) {
            Element root = this.docOut.getRootElement();
            stateEl = root.addElement("state");
        }

        stateEl.addText(state);
    }

    /**
    * Prida vitezneho hrace do xml.
    * 
    * @param winnerId
    *            id vitezneho hrace
    */
    public void addWinnerElement(int winnerId) {
        Element winnPlayer = (Element) this.docOut.selectSingleNode("/IjaGame/winner");
        if(winnPlayer == null) {
            Element root = this.docOut.getRootElement();
            winnPlayer = root.addElement("winner");
        }

        winnPlayer.addText(String.valueOf(winnerId));
    }

    /**
    * Prida do xml hrace, ktery zacinal partii
    * 
    * @param starterId
    *            id startujiciho hrace
    */
    public void addStarterElement(int starterId) {
        Element startPlayer = (Element) this.docOut.selectSingleNode("/IjaGame/starter");
        if(startPlayer == null) {
            Element root = this.docOut.getRootElement();
            startPlayer = root.addElement("starter");
        }

        startPlayer.addText(String.valueOf(String.valueOf(starterId)));
    }

    /**
    * Prida do xml hrace, ktery by mel byt aktivni po obnoveni hry
    * 
    * @param id
    *            id aktivniho hrace
    */
    public void addActivePlayerElement(int id) {
        Element actPlayer = (Element) this.docOut.selectSingleNode("/IjaGame/activePlayer");
        if(actPlayer == null) {
            Element root = this.docOut.getRootElement();
            actPlayer = root.addElement("activePlayer");
        }

        actPlayer.addText(String.valueOf(id));
    }

    /**
    * Metoda prida hrace do xml.
    * 
    * @param id
    *            id hrace
    * @param player
    *            hrac, ktery se ma pridat
    */
    public void addPlayerElement(int id, Player player) {
        if (player == null) {
                return;
        }

        // Ziskani elementu players, pripadne vytvoreni
        Element players = (Element) this.docOut.selectSingleNode("/IjaGame/players");
        if (players == null) {
                players = this.addSubRootEmptyElement("players");
        }

        // Pridani hodnot pro kazdy typ hrace
        Element actPlayer = players.addElement("player").addAttribute("id", String.valueOf(id));
        actPlayer.addElement("name").addText(player.getName());
        actPlayer.addElement("color").addText(player.getColor().toString());
        actPlayer.addElement("direction").addText(player.getDirection().toString());

        // Pridani hodnot konkretnich typu hrace
        if (player instanceof RemotePlayer) {
                RemotePlayer rp = (RemotePlayer) player;
                actPlayer.addAttribute("type", "remote");
                actPlayer.addElement("address").addText(rp.getIp());
                actPlayer.addElement("port").addText(String.valueOf(rp.getPort()));
                actPlayer.addElement("connectType").addText(rp.getConnectType().toString());
        } else if (player instanceof ComputerPlayer) {
                ComputerPlayer cp = (ComputerPlayer) player;
                actPlayer.addAttribute("type", "ai");
                actPlayer.addElement("level").addText(String.valueOf(cp.getLevel()));
        } else if (player instanceof LocalPlayer) {
                actPlayer.addAttribute("type", "local");
        }

    }

    /**
    * Metoda prida tah historie do xml.
    * 
    * @param move
    *            tah historie k ulozeni
    */
    public void addHistoryMoveElement(HistoryMove move) {
        if (move == null) {
                return;
        }

        // Ziskani elementu history, pripadne vytvoreni
        Element history = (Element) this.docOut.selectSingleNode("/IjaGame/history");
        if (history == null) {
                history = this.addSubRootEmptyElement("history");
        }

        // Notace ve ktere se bude do souboru zapisovat a ve ktere jsou uchovany
        // vnitrni objekty
        NotationReader not = NotationReaderFactory.createNotationReader(this.notationType);

        // Pridani pohybu
        Element gameMove = history.addElement("gameMove").addAttribute("id",
                        String.valueOf(move.getId()));
        gameMove.addElement("move1").addText(not.translateBack(move.getFirstMove()));
        gameMove.addElement("move2").addText(not.translateBack(move.getSecondMove()));
    }

    /**
    * Prida atribut desky popisujici barvu horniho hrace
    * 
    * @param c
    */
    public void addTopPlayerColor(FigureColor c) {
        if (c == null) {
                return;
        }

        Element desk = (Element) this.docOut.selectSingleNode("/IjaGame/desk");
        if (desk == null) {
            desk = this.addSubRootEmptyElement("desk");
        }

        desk.addAttribute("topPlayerColor", c.toString());
    }

    /**
    * Metoda prida figurku do xml.
    * 
    * @param f
    *            figurka k pridani.
    */
    public void addFigureElement(Figure f) {
        if (f == null) {
                return;
        }

        Element desk = (Element) this.docOut.selectSingleNode("/IjaGame/desk");
        if (desk == null) {
            desk = this.addSubRootEmptyElement("desk");
        }

        Element figures = (Element) desk.selectSingleNode("/IjaGame/desk/figures[@color='"
                        + f.getColor().toString() + "']");
        if (figures == null) {
            figures = desk.addElement("figures").addAttribute("color", f.getColor().toString());
        }

        Element actFigure = figures.addElement("figure").addAttribute("position",
                        f.getPosition().toString());
        if (f instanceof Piece) {
            Piece p = (Piece) f;
            actFigure.addAttribute("type", "PIECE").addAttribute("direction",
                                p.getDirection().toString());
        } else if (f instanceof Queen) {
            actFigure.addAttribute("type", "QUEEN");
        }
    }

    /**
    * Zrusi vystupni xml
    */
    public void clearOutput() {
        this.docOut.clearContent();
        this.docOut = null;
    }

    /**
    * Zrusi vstupni xml
    */
    public void clearInput() {
        this.docIn.clearContent();
        this.docIn = null;
    }

    /**
    * Metoda zapise xml do souboru.
    * 
    * @param filename
    *            nazev souboru do ktereho bude xml ulozeno.
    * @throws IOException
    */
    public void writeToFile(String filename) throws IOException {
        if (this.docOut == null) {
                this.docOut = DocumentHelper.createDocument();
                Element root = this.docOut.addElement("IjaGame");
        }

        OutputFormat format = OutputFormat.createPrettyPrint();
        XMLWriter writer = new XMLWriter(new FileWriter(filename), format);
        // XMLWriter writer = new XMLWriter(System.out, format);
        writer.write(this.docOut);
        writer.flush();
        writer.close();
    }

    /**
    * Metoda zapise xml do PrinStreamu.
    * 
    * @param output
    *            PrintStream kam se bude zapisovat
    * @throws IOException
    */
    public void writeToStream(PrintStream output) throws IOException {
        if (this.docOut == null) {
                this.docOut = DocumentHelper.createDocument();
                Element root = this.docOut.addElement("IjaGame");
        }

        OutputFormat format = OutputFormat.createPrettyPrint();
        XMLWriter writer = new XMLWriter(output, format);
        writer.write(this.docOut);
        writer.close();
    }

    // -------------------------------------------------------- //
    // ======== VNORENA TRIDA PLAYERSITERATOR ================= //
    // -------------------------------------------------------- //

    /**
    * Iterator kterym se budou prochazet vsichni hraci
    */
    public class PlayersIterator implements Iterator<Player> {

        Iterator it;

        public PlayersIterator() {
                Element root = XmlGameReaderWriter.this.docIn.getRootElement();
                List<Element> l = root.elements("players");
                this.it = l.get(0).elementIterator("player");
        }

        @Override
        public boolean hasNext() {
                return this.it.hasNext();
        }

        @Override
        public Player next() {
                Element e = (Element) this.it.next();
                // Ziskani atributu elementu
                String type = e.attributeValue("type");
                int id = Integer.parseInt(e.attributeValue("id"));
                // Naplneni hodnot konkretniho hrace
                String name = e.elementText("name");
                FigureColor color = FigureColor.getFromString(e.elementText("color"));
                FigureDirection direction = FigureDirection.getFromString(e.elementText("direction"));
                // Naplneni hodnot pro specificky typ hrace
                if (type.equals("local")) {
                        return new LocalPlayer(name, color, direction);
                } else if (type.equals("remote")) {
                        String ip = e.elementText("address");
                        int port = Integer.parseInt(e.elementText("port"));
                        RemotePlayer.ConnectType ct = RemotePlayer.ConnectType.getFromString(e
                                        .elementText("connectType"));
                        return new RemotePlayer(name, color, direction, ct, ip, port);
                } else if (type.equals("ai")) {
                        int level = Integer.parseInt(e.elementText("level"));
                        return new ComputerPlayer(name, color, direction, level);
                }

                return null;

        }

        @Override
        public void remove() {
                throw new UnsupportedOperationException("Not supported yet.");
        }

    }

    // -------------------------------------------------------- //
    // ======== VNORENA TRIDA HISTORYITERATOR ================= //
    // -------------------------------------------------------- //

    /**
    * Iterator kterym se bude prochazet historie hry.
    */
    public class HistoryIterator implements Iterator<HistoryMove> {

        Iterator it;

        public HistoryIterator() {
                Element root = XmlGameReaderWriter.this.docIn.getRootElement();
                List<Element> l = root.elements("history");
                this.it = l.get(0).elementIterator("gameMove");
        }

        @Override
        public boolean hasNext() {
                return this.it.hasNext();
        }

        @Override
        public HistoryMove next() {
                Element e = (Element) this.it.next();

                int id = Integer.parseInt(e.attributeValue("id"));
                String m1 = e.elementText("move1");
                String m2 = e.elementText("move2");

                return new HistoryMove(id, XmlGameReaderWriter.this.notationType, m1, m2);
        }

        @Override
        public void remove() {
                throw new UnsupportedOperationException("Not supported yet.");
        }

}

// -------------------------------------------------------- //
// ======== VNORENA TRIDA FIGUREITERATOR ================== //
// -------------------------------------------------------- //

/**
    * Iterator kterym se budou prochazet figurky. Slouzi k obnoveni rozehrane
    * partie. Posledni stav sachovnice - figurek je ulozen do xml a timto
    * iteratorem muze byt obnoven.
    */
    public class FigureIterator implements Iterator<FigureDescription> {

        Iterator it;
        FigureColor color;

        public FigureIterator(FigureColor c) {
                this.color = c;
                Element root = XmlGameReaderWriter.this.docIn.getRootElement();
                List<Element> l = root.elements("desk");

                if (XmlGameReaderWriter.this.docIn != null) {
                        Node node = XmlGameReaderWriter.this.docIn
                                        .selectSingleNode("IjaGame/desk/figures[@color='" + this.color.toString()
                                                        + "']");
                        if (node == null) {
                                List<FigureDescription> tmp = new ArrayList<>(0);
                                this.it = tmp.iterator();
                        } else {
                                Element e = (Element) node;
                                this.it = e.elementIterator("figure");
                        }
                }
        }

        @Override
        public boolean hasNext() {
                return this.it.hasNext();
        }

        @Override
        public FigureDescription next() {
                Element e = (Element) this.it.next();
                FigureType ft = FigureType.getFromString(e.attributeValue("type"));
                DeskCoordinate dc = new DeskCoordinate(e.attributeValue("position"));
                FigureDirection d;
                if (ft == FigureType.PIECE) {
                        d = FigureDirection.getFromString(e.attributeValue("direction"));
                } else {
                        d = FigureDirection.NONE;
                }

                return new FigureDescription(dc, ft, d, this.color);
        }

        @Override
        public void remove() {
                throw new UnsupportedOperationException("Not supported yet.");
        }

    }
}
